/*
 * Copyright 2010-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.native.interop.gen.jvm

import kotlinx.cinterop.usingJvmCInteropCallbacks
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.required
import kotlinx.metadata.klib.ChunkedKlibModuleFragmentWriteStrategy
import kotlinx.metadata.klib.KlibMetadataVersion
import org.jetbrains.kotlin.config.KlibAbiCompatibilityLevel
import org.jetbrains.kotlin.konan.ForeignExceptionMode
import org.jetbrains.kotlin.konan.TempFiles
import org.jetbrains.kotlin.konan.exec.Command
import org.jetbrains.kotlin.konan.library.*
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.Distribution
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.util.DefFile
import org.jetbrains.kotlin.library.*
import org.jetbrains.kotlin.utils.KotlinNativePaths
import org.jetbrains.kotlin.utils.usingNativeMemoryAllocator
import org.jetbrains.kotlin.library.metadata.resolver.TopologicalLibraryOrder
import org.jetbrains.kotlin.library.metadata.resolver.impl.KotlinLibraryResolverImpl
import org.jetbrains.kotlin.library.metadata.resolver.impl.libraryResolver
import org.jetbrains.kotlin.metadata.deserialization.MetadataVersion
import org.jetbrains.kotlin.native.interop.gen.*
import org.jetbrains.kotlin.native.interop.indexer.*
import org.jetbrains.kotlin.native.interop.tool.*
import org.jetbrains.kotlin.util.removeSuffixIfPresent
import org.jetbrains.kotlin.util.suffixIfNot
import org.jetbrains.kotlin.util.toCInteropKlibMetadataVersion
import java.io.File
import java.nio.file.*
import java.util.*
import kotlin.io.path.absolutePathString

data class InternalInteropOptions(val generated: String, val natives: String?, val manifest: String? = null,
                                  val cstubsName: String? = null)

fun main(args: Array<String>) {
    // Adding flavor option for interop plugin.
    class FullCInteropArguments: CInteropArguments() {
        val flavor by argParser.option(ArgType.Choice<KotlinPlatform>(), description = "Interop target")
                .default(KotlinPlatform.JVM)
        val generated by argParser.option(ArgType.String, description = "place generated bindings to the directory")
                .required()
        val natives by argParser.option(ArgType.String, description = "where to put the built native files; skips this building when empty")
    }
    val arguments = FullCInteropArguments()
    arguments.argParser.parse(args)
    val flavorName = arguments.flavor
    processCLibSafe(flavorName, arguments, InternalInteropOptions(arguments.generated, arguments.natives), runFromDaemon = false)
}

class Interop {
    /**
     * invoked via reflection from new test system: CompilationToolCallKt.invokeCInterop(),
     * `interop()` has issues to be invoked directly due to NoSuchMethodError, caused by presence of InternalInteropOptions argtype:
     * java.lang.IllegalArgumentException: argument type mismatch.
     * Also this method simplifies testing of [CInteropPrettyException] by wrapping the result in Any that acts like a "Result" class.
     * Using "Result" directly might be complicated due to signature mangle and different class loaders.
     */
    fun interopViaReflection(
            flavor: String, args: Array<String>,
            runFromDaemon: Boolean,
            generated: String, natives: String, manifest: String? = null, cstubsName: String? = null
    ): Any? {
        val internalInteropOptions = InternalInteropOptions(generated, natives, manifest, cstubsName)
        return try {
            interop(flavor, args, internalInteropOptions, runFromDaemon)
        } catch (prettyException: CInteropPrettyException) {
            prettyException
        } catch (throwable: Throwable) {
            // Return Throwable to be printed to logs by invoker, instead of allowing unprettified exception to be thrown through reflection invocation,
            // which otherwise will be replaced with InvocationTargetException with null error message and huge stacktrace which TeamCity cuts,
            // so "Caused by:" part is not saved to the log.
            throwable
        }
    }

    fun interop(
            flavor: String, args: Array<String>,
            additionalArgs: InternalInteropOptions,
            runFromDaemon: Boolean
    ): Array<String>? = when (flavor) {
        "jvm", "native" -> {
            val cinteropArguments = CInteropArguments()
            cinteropArguments.argParser.parse(args)
            val platform = KotlinPlatform.values().single { it.name.equals(flavor, ignoreCase = true) }
            processCLibSafe(platform, cinteropArguments, additionalArgs, runFromDaemon)
        }
        else -> error("Unexpected flavor")
    }
}
// Options, whose values are space-separated and can be escaped.
val escapedOptions = setOf("-compilerOpts", "-linkerOpts", "-compiler-options", "-linker-options")

private fun String.asArgList(key: String) =
        if (escapedOptions.contains(key))
            this.split(Regex("(?<!\\\\)\\Q \\E")).filter { it.isNotEmpty() }.map { it.replace("\\ ", " ") }
        else
            listOf(this)

private fun <T> Collection<T>.atMostOne(): T? {
    return when (this.size) {
        0 -> null
        1 -> this.iterator().next()
        else -> throw IllegalArgumentException("Collection has more than one element.")
    }
}

private fun List<String>?.isTrue(): Boolean {
    // The rightmost wins, null != "true".
    return this?.last() == "true"
}

private fun runCmd(command: Array<String>, verbose: Boolean = false, redirectInputFile: File? = null) {
    if (verbose) {
        val redirect = if (redirectInputFile == null) "" else " < ${redirectInputFile.path}"
        println("COMMAND: " + command.joinToString(" ") + redirect)
    }
    Command(command.toList(), redirectInputFile = redirectInputFile).getOutputLines(true).let { lines ->
        if (verbose) lines.forEach(::println)
    }
}

private fun Properties.storeProperties(file: File) {
    file.outputStream().use {
        this.store(it, null)
    }
}

private fun Properties.putAndRunOnReplace(key: Any, newValue: Any, beforeReplace: (Any, Any, Any) -> Unit) {
    val oldValue = this[key]
    if (oldValue != null && oldValue != newValue) {
        beforeReplace(key, oldValue, newValue)
    }
    this[key] = newValue
}

private fun selectNativeLanguage(config: DefFile.DefFileConfig): Language {
    val languages = mapOf(
            "C" to Language.C,
            "C++" to Language.CPP,
            "Objective-C" to Language.OBJECTIVE_C
    )

    // C++ is not publicly supported.
    val publicLanguages = languages.keys.minus("C++")

    val lang = config.language?.let {
        languages[it]
                ?: error("Unexpected language '${config.language}'. Possible values are: ${publicLanguages.joinToString { "'$it'" }}")
    } ?: Language.C

    return lang

}

private fun parseImports(dependencies: List<KotlinLibrary>): ImportsImpl =
        dependencies.mapNotNull { library ->
            // TODO: handle missing properties?
            library.packageFqName?.let { packageFqName ->
                val headerIds = library.includedHeaders
                headerIds.map { HeaderId(it) to PackageInfo(packageFqName, library) }
            }
        }.reversed().flatten().toMap().let(::ImportsImpl)

fun getCompilerFlagsForVfsOverlay(headerFilterPrefix: Array<String>, def: DefFile): List<String> {
    val relativeToRoot = mutableMapOf<Path, Path>() // TODO: handle clashes

    val filteredIncludeDirs = headerFilterPrefix .map { Paths.get(it) }
    if (filteredIncludeDirs.isNotEmpty()) {
        val headerFilterGlobs = def.config.headerFilter
        val excludeFilterGlobs = def.config.excludeFilter
        if (headerFilterGlobs.isEmpty()) {
            error("'$HEADER_FILTER_ADDITIONAL_SEARCH_PREFIX' option requires " +
                    "'headerFilter' to be specified in .def file")
        }

        relativeToRoot += findFilesByGlobs(roots = filteredIncludeDirs, includeGlobs = headerFilterGlobs, excludeGlobs = excludeFilterGlobs)
    }

    if (relativeToRoot.isEmpty()) {
        return emptyList()
    }

    val virtualRoot = Paths.get(System.getProperty("java.io.tmpdir")).resolve("konanSystemInclude")

    val virtualPathToReal = relativeToRoot.map { (relativePath, realRoot) ->
        virtualRoot.resolve(relativePath) to realRoot.resolve(relativePath)
    }.toMap()

    val vfsOverlayFile = createVfsOverlayFile(virtualPathToReal)

    return listOf("-I${virtualRoot.toAbsolutePath()}", "-ivfsoverlay", vfsOverlayFile.toAbsolutePath().toString())
}

private fun findFilesByGlobs(roots: List<Path>, includeGlobs: List<String>, excludeGlobs: List<String>): Map<Path, Path> {
    val relativeToRoot = mutableMapOf<Path, Path>()

    val pathMatchers = includeGlobs.map { FileSystems.getDefault().getPathMatcher("glob:$it") }
    val excludePathMatchers = excludeGlobs.map { FileSystems.getDefault().getPathMatcher("glob:$it") }

    roots.reversed()
            .filter { path ->
                return@filter when {
                    path.toFile().exists() -> true
                    else -> { warn("$path doesn't exist"); false }
                }
            }
            .forEach { root ->
                // TODO: don't scan the entire tree, skip subdirectories according to globs.
                Files.walk(root, FileVisitOption.FOLLOW_LINKS).forEach { path ->
                    val relativePath = root.relativize(path)
                    val shouldInclude = !Files.isDirectory(path)
                            && excludePathMatchers.all { !it.matches(relativePath) }
                            && pathMatchers.any { it.matches(relativePath) }
                    if (shouldInclude) {
                        relativeToRoot[relativePath] = root
                    }
                }
            }
    return relativeToRoot
}

private fun processCLibSafe(flavor: KotlinPlatform, cinteropArguments: CInteropArguments,
                            additionalArgs: InternalInteropOptions, runFromDaemon: Boolean) =
        usingNativeMemoryAllocator {
            usingJvmCInteropCallbacks {
                processCLib(flavor, cinteropArguments, additionalArgs, runFromDaemon)
            }
        }

private fun processCLib(
        flavor: KotlinPlatform,
        cinteropArguments: CInteropArguments,
        additionalArgs: InternalInteropOptions,
        runFromDaemon: Boolean,
): Array<String>? = withExceptionPrettifier(cinteropArguments.disableExceptionPrettifier) {
    val ktGenRoot = additionalArgs.generated
    val nativeLibsDir = additionalArgs.natives
    val defFile = cinteropArguments.def?.let { File(it) }
    val manifestAddend = additionalArgs.manifest?.let { File(it) }

    if (defFile == null && cinteropArguments.pkg == null) {
        cinteropArguments.argParser.printError("-def or -pkg should be provided!")
    }

    val tool = prepareTool(cinteropArguments.target, flavor, runFromDaemon, parseKeyValuePairs(cinteropArguments.overrideKonanProperties), konanDataDir = cinteropArguments.konanDataDir)

    val def = DefFile(defFile, tool.target)

    checkCCallModeCompatibility(cinteropArguments, def)
    checkKlibAbiCompatibilityLevel(cinteropArguments)

    val isLinkerOptsSetByUser = (cinteropArguments.linkerOpts.valueOrigin == ArgParser.ValueOrigin.SET_BY_USER) ||
            (cinteropArguments.linkerOptions.valueOrigin == ArgParser.ValueOrigin.SET_BY_USER) ||
            (cinteropArguments.linkerOption.valueOrigin == ArgParser.ValueOrigin.SET_BY_USER)
    if (flavor == KotlinPlatform.NATIVE && isLinkerOptsSetByUser) {
        warn("-linker-option(s)/-linkerOpts option is not supported by cinterop. Please add linker options to .def file or binary compilation instead.")
    }

    val additionalLinkerOpts = cinteropArguments.linkerOpts.value.toTypedArray() + cinteropArguments.linkerOption.value.toTypedArray() +
            cinteropArguments.linkerOptions.value.toTypedArray()
    val verbose = cinteropArguments.verbose

    val entryPoint = def.config.entryPoints.atMostOne()
    val linkerName = cinteropArguments.linker ?: def.config.linker
    val linker = "${tool.llvmHome}/bin/$linkerName"
    val compiler = "${tool.llvmHome}/bin/clang"
    val excludedFunctions = def.config.excludedFunctions.toSet()
    val excludedMacros = def.config.excludedMacros.toSet()
    val staticLibraries = def.config.staticLibraries + cinteropArguments.staticLibrary.toTypedArray()
    val projectDir = cinteropArguments.projectDir
    val libraryPaths = (def.config.libraryPaths + cinteropArguments.libraryPath).map {
        if (projectDir == null || Paths.get(it).isAbsolute)
            it
        else Paths.get(projectDir, it).absolutePathString()
    }
    val fqParts = (cinteropArguments.pkg ?: def.config.packageName)?.split('.')
            ?: defFile!!.name.split('.').reversed().drop(1)

    val outKtPkg = fqParts.joinToString(".")

    val resolver = getLibraryResolver(cinteropArguments, tool.target)

    val allLibraryDependencies = when (flavor) {
        KotlinPlatform.NATIVE -> resolveDependencies(resolver, cinteropArguments)
        else -> listOf()
    }

    val libName = additionalArgs.cstubsName ?: fqParts.joinToString("") + "stubs"

    val tempFiles = TempFiles(cinteropArguments.tempDir)

    val imports = parseImports(allLibraryDependencies)

    val library = buildNativeLibrary(tool, def, cinteropArguments, imports)

    // when this tool does not compile native library, make the generated source consumable by external compiler (i.e. do not strip includes)
    val (nativeIndex, compilation) = buildNativeIndexImpl(library, verbose, allowPrecompiledHeaders = nativeLibsDir != null)

    val target = tool.target

    val klibSuffix = CompilerOutputKind.LIBRARY.suffix(target)
    val moduleName = cinteropArguments.moduleName
            ?: File(cinteropArguments.output).name.removeSuffixIfPresent(klibSuffix)

    val configuration = InteropConfiguration(
            library = compilation,
            pkgName = outKtPkg,
            excludedFunctions = excludedFunctions,
            excludedMacros = excludedMacros,
            strictEnums = def.config.strictEnums.toSet(),
            nonStrictEnums = def.config.nonStrictEnums.toSet(),
            noStringConversion = def.config.noStringConversion.toSet(),
            exportForwardDeclarations = def.config.exportForwardDeclarations,
            allowedOverloadsForCFunctions = def.config.allowedOverloadsForCFunctions.toSet(),
            disableDesignatedInitializerChecks = def.config.disableDesignatedInitializerChecks,
            disableExperimentalAnnotation = cinteropArguments.disableExperimentalAnnotation ?: false,
            target = target,
            cCallMode = cinteropArguments.cCallMode,
    )


    nativeLibsDir?.let { File(it).mkdirs() }
    val outCFile = tempFiles.create(libName, ".${library.language.sourceFileExtension}")

    val logger = if (verbose) {
        { message: String -> println(message) }
    } else {
        {}
    }
    val mode = when (flavor) {
        KotlinPlatform.JVM -> GenerationMode.SOURCE_CODE
        KotlinPlatform.NATIVE -> GenerationMode.METADATA
    }
    // when this tool does not compile native library, make the generated source consumable by external compiler (i.e. do not strip includes)
    val stubIrContext = StubIrContext(
            logger,
            configuration,
            nativeIndex,
            imports,
            flavor,
            mode,
            libName,
            allowPrecompiledHeaders = nativeLibsDir != null,
            KlibMetadataVersion(cinteropArguments.klibAbiCompatibilityLevel.toCInteropKlibMetadataVersion().toArray())
    )
    val stubIrOutput = run {
        val outKtFileCreator = {
            val outKtFileName = fqParts.last() + ".kt"
            val outKtFileRelative = (fqParts + outKtFileName).joinToString("/")
            val file = File(ktGenRoot, outKtFileRelative)
            file.parentFile.mkdirs()
            file
        }
        val driverOptions = StubIrDriver.DriverOptions(
                entryPoint,
                moduleName,
                File(outCFile.absolutePath),
                outKtFileCreator,
                cinteropArguments.dumpBridges ?: false,
        )
        val stubIrDriver = StubIrDriver(stubIrContext, driverOptions)
        stubIrDriver.run()
    }

    // TODO: if a library has partially included headers, then it shouldn't be used as a dependency.
    def.manifestAddendProperties["includedHeaders"] = nativeIndex.includedHeaders.joinToString(" ") { it.value }

    def.manifestAddendProperties.putAndRunOnReplace("package", outKtPkg) {
        _, oldValue, newValue ->
            warn("The package value `$oldValue` specified in .def file is overridden with explicit $newValue")
    }
    def.manifestAddendProperties[KLIB_PROPERTY_INTEROP] = "true"
    if (stubIrOutput is StubIrDriver.Result.Metadata) {
        def.manifestAddendProperties[KLIB_PROPERTY_IR_PROVIDER] = KLIB_INTEROP_IR_PROVIDER_IDENTIFIER
    }
    stubIrContext.addManifestProperties(def.manifestAddendProperties)
    // cinterop command line option overrides def file property
    val foreignExceptionMode = cinteropArguments.foreignExceptionMode?: def.config.foreignExceptionMode
    foreignExceptionMode?.let {
        def.manifestAddendProperties[ForeignExceptionMode.manifestKey] =
                ForeignExceptionMode.byValue(it).value   // may throw IllegalArgumentException
    }

    cinteropArguments.userSetupHint?.let {
        def.manifestAddendProperties.put("userSetupHint", it)?.also {
            warn("User setup hint provided in .def file will be shadowed by command line argument")
        }
    }

    manifestAddend?.parentFile?.mkdirs()
    manifestAddend?.let { def.manifestAddendProperties.storeProperties(it) }

    if (nativeLibsDir == null) {
        check(flavor == KotlinPlatform.JVM) { "Only JVM cinterop can skip native library generation" }
        check(stubIrOutput is StubIrDriver.Result.SourceCode) { "Native library generation can only be skipped when sources are generated" }
        return null
    }

    val compilerArgs = stubIrContext.libraryForCStubs.compilerArgs.toTypedArray()
    val nativeOutputPath: String? = when (flavor) {
        KotlinPlatform.JVM -> {
            val outOFile = tempFiles.create(libName,".o")
            val compilerCmd = arrayOf(compiler, *compilerArgs,
                    "-c", outCFile.absolutePath, "-o", outOFile.absolutePath)
            runCmd(compilerCmd, verbose)
            val linkerOpts =
                    def.config.linkerOpts.toTypedArray() +
                            tool.getDefaultCompilerOptsForLanguage(library.language) +
                            additionalLinkerOpts
            val outLib = File(nativeLibsDir, System.mapLibraryName(libName))
            val linkerCmd = arrayOf(linker,
                    outOFile.absolutePath, "-shared", "-o", outLib.absolutePath,
                    *linkerOpts)
            runCmd(linkerCmd, verbose)
            outOFile.absolutePath
        }
        KotlinPlatform.NATIVE if configuration.cCallMode == CCallMode.DIRECT -> {
            // Don't generate the bitcode (and don't pack it into the resulting klib),
            // because indirect CCall is the main reason for having it.
            // We could introduce another flag to control that, but let's keep things simple for now.
            null
        }
        KotlinPlatform.NATIVE -> {
            val outLib = File(nativeLibsDir, "$libName.bc")
            // Note that the output bitcode contains the source file path, which can lead to non-deterministc builds (see KT-54284).
            // The source file is passed in via stdin to ensure the output library is deterministic.
            val compilerCmd = arrayOf(compiler, *compilerArgs,
                    "-emit-llvm", "-x", library.language.clangLanguageName, "-c", "-", "-o", outLib.absolutePath, "-Xclang", "-detailed-preprocessing-record")
            runCmd(compilerCmd, verbose, redirectInputFile = File(outCFile.absolutePath))
            outLib.absolutePath
        }
    }

    val compiledFiles = compileSources(nativeLibsDir, tool, cinteropArguments)

    return when (stubIrOutput) {
        is StubIrDriver.Result.SourceCode -> {
            val bitcodePaths = compiledFiles.map {  listOf("-native-library", it) }.flatten()
            argsToCompiler(staticLibraries, libraryPaths) + bitcodePaths
        }
        is StubIrDriver.Result.Metadata -> {
            val stdlibDependency = resolver.resolveWithDependencies(
                    emptyList(),
                    noDefaultLibs = true,
                    noEndorsedLibs = true
            ).getFullList()

            val nopack = cinteropArguments.nopack
            val outputPath = cinteropArguments.output.let {
                val suffix = CompilerOutputKind.LIBRARY.suffix(tool.target)
                if (nopack) it.removeSuffixIfPresent(suffix) else it.suffixIfNot(suffix)
            }

            val serializedMetadata = stubIrOutput.metadata.write(ChunkedKlibModuleFragmentWriteStrategy(topLevelClassifierDeclarationsPerFile = 128)).run {
                SerializedMetadata(header, fragments, fragmentNames, metadataVersion.toArray())
            }

            createInteropLibrary(
                    serializedMetadata = serializedMetadata,
                    nativeBitcodeFiles = compiledFiles + listOfNotNull(nativeOutputPath),
                    target = tool.target,
                    moduleName = moduleName,
                    outputPath = outputPath,
                    manifest = def.manifestAddendProperties,
                    dependencies = stdlibDependency + imports.requiredLibraries.toList(),
                    nopack = nopack,
                    shortName = cinteropArguments.shortModuleName,
                    staticLibraries = resolveLibraries(staticLibraries, libraryPaths),
                    klibAbiCompatibilityLevel = cinteropArguments.klibAbiCompatibilityLevel,
            )
            return null
        }
    }
}

private fun checkCCallModeCompatibility(
        cinteropArguments: CInteropArguments,
        def: DefFile
) {
    if (cinteropArguments.cCallMode == CCallMode.INDIRECT) return

    /*
    The two features below rely on including bitcode to the resulting cinterop klib.
    Unless `-Xccall-mode indirect` is used, the resulting cinterop klib can be used without bitcode:
    - `-Xccall-mode direct` makes the cinterop klib not include any bitcode.
    - `-Xccall-mode both` makes the compiler able to use it with `-Xbinary=cCallMode=direct`
      and therefore ignore the included bitcode.

    In other words, these two features are compatible only with `-Xccall-mode indirect`.
    Additionally, regardless of the bitcode inclusion, the compiler also doesn't support generating direct CCalls to
    functions defined through `compileSource`.
    */
    val flag = "-$CCALL_MODE ${CCallMode.INDIRECT.name.lowercase()}"
    check(def.config.entryPoints.isEmpty()) {
        "entryPoint= in the .def file is only supported with the legacy mode flag $flag.\n" +
                "See https://youtrack.jetbrains.com/issue/KT-79747 for more details."
    }

    check(cinteropArguments.compileSource.isEmpty()) {
        "-$COMPILE_SOURCES is only supported with the legacy mode flag $flag.\n" +
                "See https://youtrack.jetbrains.com/issue/KT-79749 for more details."
    }
}

// TODO (KT-81433): Reconsider how exactly the export in P.V. feature should work in further versions (ex: 2.4.0)
//  if we decide to upgrade LLVM.
private fun checkKlibAbiCompatibilityLevel(cinteropArguments: CInteropArguments) {
    val klibAbiCompatibilityLevel = cinteropArguments.klibAbiCompatibilityLevel
    val cCallMode = cinteropArguments.cCallMode

    when (klibAbiCompatibilityLevel) {
        KlibAbiCompatibilityLevel.ABI_LEVEL_2_2 -> {
            check(cCallMode == CCallMode.INDIRECT) {
                "-$CCALL_MODE ${cCallMode.name.lowercase()} is not supported in combination with -$KLIB_ABI_COMPATIBILITY_LEVEL ${klibAbiCompatibilityLevel}\n" +
                        "Please use -$KLIB_ABI_COMPATIBILITY_LEVEL ${KlibAbiCompatibilityLevel.LATEST_STABLE} or specify -$CCALL_MODE ${CCallMode.INDIRECT.name.lowercase()}"
            }

            warn("-$KLIB_ABI_COMPATIBILITY_LEVEL $klibAbiCompatibilityLevel will trigger generating KLIB compatible with KLIB ABI version $klibAbiCompatibilityLevel. This is an experimental feature.")
        }

        KlibAbiCompatibilityLevel.ABI_LEVEL_2_3 -> {
            // No specific restrictions for now.
        }
    }
}

private fun compileSources(
        nativeLibsDir: String,
        toolConfig: ToolConfig,
        cinteropArguments: CInteropArguments
): List<String> = cinteropArguments.compileSource.mapIndexed { index, source ->
    // Mangle file name to avoid collisions.
    val mangledFileName = "${index}_${File(source).nameWithoutExtension}"
    val outputFileName = "$nativeLibsDir/${mangledFileName}.bc"
    val compilerArgs = cinteropArguments.sourceCompileOptions.toTypedArray()
    val compilerCmd = toolConfig.clang.clangCXX(*compilerArgs, source, "-emit-llvm", "-c", "-o", outputFileName)
    runCmd(compilerCmd.toTypedArray(), verbose = cinteropArguments.verbose)
    outputFileName
}

private fun getLibraryResolver(
        cinteropArguments: CInteropArguments, target: KonanTarget
): KotlinLibraryResolverImpl<KotlinLibrary> {
    return defaultResolver(
        directLibs = cinteropArguments.library,
        target,
        Distribution(KotlinNativePaths.homePath.absolutePath, konanDataDir = cinteropArguments.konanDataDir)
    ).libraryResolver(resolveManifestDependenciesLenient = true)
}

private fun resolveDependencies(
        resolver: KotlinLibraryResolverImpl<KotlinLibrary>, cinteropArguments: CInteropArguments
): List<KotlinLibrary> {
    val noDefaultLibs = cinteropArguments.nodefaultlibs || cinteropArguments.nodefaultlibsDeprecated
    val noEndorsedLibs = cinteropArguments.noendorsedlibs
    val resolvedLibraries = resolver.resolveWithDependencies(
        unresolvedLibraries = cinteropArguments.library.toUnresolvedLibraries,
        noStdLib = false,
        noDefaultLibs = noDefaultLibs,
        noEndorsedLibs = noEndorsedLibs
    ).getFullList(TopologicalLibraryOrder)
    validateNoLibrariesWerePassedViaCliByUniqueName(cinteropArguments.library, resolvedLibraries, resolver.logger)
    return resolvedLibraries
}

internal fun prepareTool(target: String?, flavor: KotlinPlatform, runFromDaemon: Boolean, propertyOverrides: Map<String, String> = emptyMap(), konanDataDir: String? = null) =
        ToolConfig(target, flavor, propertyOverrides, konanDataDir).also {
            if (!runFromDaemon) it.prepare() // Daemon prepares the tool himself. (See KonanToolRunner.kt)
        }

internal val predefinedObjCClassesIncludingCategories: Set<String> by lazy { setOf("NSView", "UIView") }

internal fun buildNativeLibrary(
        tool: ToolConfig,
        def: DefFile,
        arguments: CInteropArguments,
        imports: Imports
): NativeLibrary {
    val additionalHeaders = (arguments.header).toTypedArray()
    val additionalCompilerOpts = (arguments.compilerOpts +
            arguments.compilerOptions + arguments.compilerOption).toTypedArray()

    val headerFiles = def.config.headers + additionalHeaders
    val language = selectNativeLanguage(def.config)
    val compilerOpts: List<String> = mutableListOf<String>().apply {
        addAll(def.config.compilerOpts)
        addAll(tool.getDefaultCompilerOptsForLanguage(language))
        addAll(additionalCompilerOpts)
        addAll(getCompilerFlagsForVfsOverlay(arguments.headerFilterPrefix.toTypedArray(), def))
        add("-Wno-builtin-macro-redefined") // to suppress warning from predefinedMacrosRedefinitions(see below)
    }

    // Expanding macros such as __FILE__ or __TIME__ exposes arbitrary generated filenames and timestamps from the compiler pipeline
    // which are not useful for interop though makes the klib generation non-deterministic. See KT-54284
    // This macro redefinition just maps to their name in the properties available from Kotlin.
    val predefinedMacrosRedefinitions = predefinedMacros.map {
        "#define $it \"$it\""
    }

    val compilation = CompilationImpl(
            includes = headerFiles.map { IncludeInfo(it, null) },
            additionalPreambleLines = def.defHeaderLines + predefinedMacrosRedefinitions,
            compilerArgs = defaultCompilerArgs(language) + compilerOpts + tool.platformCompilerOpts,
            language = language
    )

    val headerFilter: NativeLibraryHeaderFilter
    val includes: List<IncludeInfo>

    val modules = def.config.modules

    if (modules.isEmpty()) {
        require(headerFiles.isEmpty() || !compilation.compilerArgs.contains("-fmodules")) { "cinterop doesn't support having headers in -fmodules mode" }
        val excludeDependentModules = def.config.excludeDependentModules

        val headerFilterGlobs = def.config.headerFilter
        val excludeFilterGlobs = def.config.excludeFilter
        val headerInclusionPolicy = HeaderInclusionPolicyImpl(headerFilterGlobs, excludeFilterGlobs)

        headerFilter = NativeLibraryHeaderFilter.NameBased(headerInclusionPolicy, excludeDependentModules)
        includes = headerFiles.map { IncludeInfo(it, null) }
    } else {
        require(language == Language.OBJECTIVE_C) { "cinterop supports 'modules' only when 'language = Objective-C'" }
        require(headerFiles.isEmpty()) { "cinterop doesn't support having headers and modules specified at the same time" }
        require(def.config.headerFilter.isEmpty()) { "cinterop doesn't support 'headerFilter' with 'modules'" }

        val modulesInfo = getModulesInfo(compilation, modules)

        headerFilter = NativeLibraryHeaderFilter.Predefined(modulesInfo.ownHeaders, modulesInfo.modules)
        includes = modulesInfo.topLevelHeaders
    }

    val excludeSystemLibs = def.config.excludeSystemLibs

    val headerExclusionPolicy = HeaderExclusionPolicyImpl(imports)

    val objCClassesIncludingCategories = def.config.objcClassesIncludingCategories.toSet() + predefinedObjCClassesIncludingCategories
    return NativeLibrary(
            includes = includes,
            additionalPreambleLines = compilation.additionalPreambleLines,
            compilerArgs = compilation.compilerArgs,
            headerToIdMapper = HeaderToIdMapper(sysRoot = tool.sysRoot),
            language = compilation.language,
            excludeSystemLibs = excludeSystemLibs,
            headerExclusionPolicy = headerExclusionPolicy,
            headerFilter = headerFilter,
            objCClassesIncludingCategories = objCClassesIncludingCategories,
            allowIncludingObjCCategoriesFromDefFile = def.config.allowIncludingObjCCategoriesFromDefFile,
    )
}

fun parseKeyValuePairs(
    argumentValue: List<String>,
): Map<String, String> = argumentValue.mapNotNull {
    val keyValueSeparatorIndex = it.indexOf('=')
    if (keyValueSeparatorIndex > 0) {
        it.substringBefore('=') to it.substringAfter('=')
    } else {
        warn("incorrect property format: expected '<key>=<value>', got '$it'")
        null
    }
}.toMap()
